v2.1.0-beta: dependency upgrade, CTLie integration, test rework - #826
Merged
Conversation
The package did not load: `UndefVarError: AbstractDiscretizer not defined in CTDirect`. Three symbol families physically changed package while OptimalControl stayed pinned at CTBase 0.18.8 / CTModels 0.10.1. Re-point every import at its *owning* submodule, per the Handbook `modules.md` rule: CTFlows → CTBase.Data the type vocabulary `Flow` dispatches on CTFlows → CTLie ad / Lift / Poisson / ∂ₜ / @lie CTSolvers → CTBase.Strategies, CTBase.Options, CTBase.Orchestration CTDirect → CTSolvers.DOCP AbstractDiscretizer, discretize CTModels → Components / Models / Solutions / Building / Init New `src/imports/{ctlie,adnlpmodels,ad}.jl`. The last two exist for their imports alone: a `[deps]` entry arms no extension — Julia fires one when the trigger package is *loaded*. ADNLPModels and DifferentiationInterface are in `[deps]` precisely because CTSolvers' and CTBase's extensions need them, so we must load them or ship a dead capability. `test/suite/extensions/` guards this. Also re-points ~45 qualified sites across `src/helpers/` and `src/solve/`. Argument annotations are load-fatal while bodies fail only on first call, so precompilation proves nothing here; verified by forcing the lazy registry, `describe`, the print helpers and a descriptive `solve` end to end. Two API changes are not renames, and are handled rather than papered over: - `CTSolvers.Strategies.get_parameter_type` → `CTBase.Strategies.parameter` returns `nothing` no longer: it throws `NotImplemented` when a strategy has not implemented the (optional) parameter contract. `print.jl` is display code and must not be what crashes on a third-party strategy, hence the new `_strategy_parameter` shim. - `time` and `success` are dropped from the re-export surface. Both resolve to bare `Base` functions: `CTModels.Components` extends `Base.time` without exporting it, and `CTModels.Solutions` exports the *name* `success` while defining no method for it — `success(sol)` was always a `MethodError`. The real accessor is `successful`. Refs #815 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`Test.@test isdefined(OptimalControl, :foo)` is vacuously true for every name that also exists in `Base` — `time`, `merge`, `status`, `name`, `value`, `describe`, `success`. Julia resolves those through the implicit `using Base`, so the whole reexport group could go green while OptimalControl re-exported none of them. That is exactly the blind spot this migration walks through. New `test/helpers/reexport.jl` with `reexports` / `imports` / `is_exported`, which check the binding resolves to the object the owning submodule defines. Rewrites `test_ctflows.jl` (nearly every assertion was stale) and `test_ctsolvers.jl`, adds `test_ctlie.jl`, and reorganises `test_ctbase.jl` and `test_ctmodels.jl` by owning submodule. `test_ctdirect.jl` is recentred on what is still CTDirect's — the concrete discretizers — since the names it asserted kept passing straight through the move to CTSolvers.DOCP without noticing it. Three corrections to what the migration report assumed, found by running this: - `OpenLoop`/`ClosedLoop`/`DynClosedLoop` and `StateConstraint`/ `ControlConstraint`/`MixedConstraint` are *factory functions*, not types. They all build a `ControlLaw{F,Kind,…}` / `PathConstraint{F,Kind,…}`; the kind is a trait parameter. `OpenLoop <: AbstractControlLaw` is a `TypeError`. - `Flow` is a `UnionAll`, not a `Function`. - `ocp_solution` takes a `BuiltModel`, not a `DiscretizedModel`. Refs #815 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mechanical re-pointing of ~113 stale qualified sites, plus the `CTBase` imports the new paths need. `is_parameter_type` is deprecated upstream in favour of `is_a_parameter`; switch rather than emit depwarns. The `parameter` assertions needed real work, not a rename: `parameter(ADNLP)` throws `NotImplemented` where `get_parameter_type(ADNLP)` returned `nothing`. A bare `UnionAll` genuinely does not determine a parameter — it is the instantiated type that carries one. Assert both halves. New `test/helpers/capabilities.jl`, mirroring the upstream `TestCapabilities` pattern. `is_cuda_on` was defined three times independently (runtests.jl — unused, test_canonical.jl, test_options_forwarding.jl); this is now the single definition, and it separates two things that are easy to conflate: `gpu_extension_armed()` (is the extension loaded — CPU-runnable) from `is_cuda_on()` (is there a device). The CUDA device tier now uses `Test.@test_skip` instead of a silent `if is_cuda_on()`, so the skip appears in the summary rather than vanishing. Refs #815 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Mechanical re-pointing of ~152 stale qualified sites. The `const MOCK_REGISTRY`
in `test_descriptive_routing.jl` is load-fatal for the whole file, so it went
first.
The real find: every mock strategy was out of contract. `parameter` must now be
implemented by each strategy — the CTBase generic throws `NotImplemented` by
default, where the old `CTSolvers.Strategies.get_parameter_type` silently
returned `nothing`. `route_all_options` calls it, so option routing failed
outright and `solve_descriptive` errored on every bypass test. The real
strategies all implement it, which is why the end-to-end solve kept working and
the failure looked like a src regression.
`MockModelerParam`/`MockSolverParam` now report the parameter they actually
carry, rather than `nothing` — the old assertion only held because nothing
implemented the contract at all.
`test_canonical.jl` gains `using CUDSS`: since CTSolvers#189 the extension
trigger is `["MadNLPGPU", "CUDA", "CUDSS"]` — all three. Without it
`CTSolversMadNLPGPU` never loads and `MadNLP{GPU}` is not registered as a
strategy, silently. (CUDSS loads fine on macOS despite linux/windows-only
artifacts: the artifact is lazy.)
`test_explicit.jl` imported MadNLPGPU and CUDA but never used them; dropped.
Refs #815
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three user-visible breaks, exercised together on Goddard: 1. `Lie(X, f)` → `ad(X, f)`, from CTLie. `X ⋅ f` was dropped with no replacement. `Lift`/`@Lie` also moved to CTLie. 2. Constrained flows take *paired keywords*: `Flow(ocp, u; constraint=g, multiplier=μ)`. The three-positional form is a `MethodError`; one keyword without the other is an `IncorrectArgument` (the migration report said `PreconditionError` — it is not). 3. `variable=` is mandatory on a `NonFixed` flow. Goddard and the time-minimal double integrator both declare `tf ∈ R, variable`, so all four call sites need it; omitting it raises a `PreconditionError` rather than defaulting. `Flow` also needs an integrator loaded now (Q7), hence the `OrdinaryDiffEqTsit5` imports. Goddard additionally needs `CTLie` and `CTBase` imported explicitly: `@Lie` expands to bare `CTLie.*` and `CTBase.Traits.*` prefixes, and while OptimalControl does re-export both, this file imports qualified (`using OptimalControl: OptimalControl`), which brings in that one name only. Same reason it used to carry `import CTFlows: CTFlows`. Guard rails added for each break, so a regression fails loudly rather than silently changing meaning. Refs #815 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lity
The nine problems returned bare NamedTuples with **no common schema** — some
carried `F0`/`F1`, some `x0`/`xf`/`t0`/`tf`, some `p_expected`. Anything
generic over them had to guess.
`TestProblem` promotes the four fields every problem has and keeps the rest in
`data`, verbatim: no information was dropped, it moved one level down.
Each problem now builds two ways, `:abstract` (the `@def` DSL) and
`:functional` (the `CTModels.Building` API), which is what makes a
form-equivalence test possible at all. 18/18 build.
Also adds a `methods` field, prompted by review: not every problem can be
attacked both ways. The quadrotor has no exploitable extremal structure and no
reference costate, so it is a direct fixture only — and nothing said so. A
shooting sweep would have had to skip it by name, a list that rots.
The claim is self-enforcing rather than documentary: declaring `:indirect`
without a `p0` in `data` is rejected by the constructor. Selection goes through
`problems_for(:indirect)`.
indirect: goddard, double_integrator_{time,energy,energy_constrained}
direct: all nine
The indirect tests now read `p0` and the switching times from `data` instead of
restating them, so the reference data cannot drift away from the model that is
supposed to satisfy it.
Two things the functional API insists on, both easy to get wrong:
`time_dependence!` is mandatory before `build`, and a control-free problem must
*omit* `control!` — `control!(pre, 0)` is rejected.
Refs #815
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four new groups, each covering a boundary OptimalControl owns rather than one
its dependencies already test.
**suite/problems/test_forms_equivalent.jl** — `@def` is sugar over
`CTModels.Building`, and if the sugar and the API drift apart one of them is
lying. One body over all nine problems: dimensions, horizon kind, cost,
dynamics at a sample point, constraint dimensions. The `definition` difference
is asserted *as* a difference rather than skipped.
**suite/problems/test_hamiltonian_type.jl** — `:total` and `:partial` agree
wherever the control law is stationary for H̃, which is everywhere in the
indirect suite, so it is easy to believe they are redundant. They are not, and
this pins down why: on the energy double integrator with u = p₂ + 1 (not the
minimiser),
:partial → ẋ₂ = u = p₂ + 1 ("apply this feedback")
:total → ẋ₂ = u + (∂H̃/∂u)(∂u/∂p₂) = p₂ (the perturbation cancels)
Both correct for what they compute; picking the wrong one on a non-stationary
law silently integrates different dynamics. Also runs the stationary cases
across both front ends, including Goddard's constrained arc.
**suite/flows/test_flow_api.jl** — the constructor grid, the three `constraint`
spellings including the `Symbol` label that reuses the OCP's own declared
constraint, and the calling convention.
Two things found while writing it:
- `Flow(ocp, ClosedLoop/OpenLoop)` returns a *state* flow with no costate, not
a rejection — the report had this as a `PreconditionError`.
- `is_autonomous` strips the time argument uniformly across law kinds, so an
autonomous `OpenLoop` is called with **no arguments at all**. `OpenLoop(t ->
0.0)` reads as correct and is a `MethodError` at integration time. The full
arity table is now pinned; the design question is raised neutrally in
control-toolbox/CTBase.jl#515.
**suite/shape/test_shape_contract.jl** — deliberately *not* a copy of
CTDirect's exhaustive per-scheme version. What is ours is the contract
surviving the whole `solve` stack, the same contract on the indirect path
(CTFlows has no assertion for it, only defensive `isa Number ? … : …[1]`
guards that tolerate either shape), and the two paths agreeing — the seam
nothing upstream can check.
`test/helpers/shooting.jl` ports `test_shooting` from CTFlows so the indirect
checks here ask the same two questions upstream does: residual at the known
solution (a wrong derivation still converges, just to the wrong thing), and
convergence from a perturbed guess.
Refs #815
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…v2.1.0-beta
`:cpu`/`:gpu` now means the same thing on both sides of the library, so the
routing is worth asserting on both — and all of it is CPU-runnable: resolving
a GPU strategy builds a parameterized type, it does not execute on a device.
Two things kept carefully apart, because conflating them is how a GPU suite
ends up green and inert:
extension armed — is `CTSolversMadNLPGPU` loaded? CPU-runnable, and the only
local evidence the GPU path is compiled in at all.
device present — is there a functional GPU? False on every local runner.
Since CTSolvers#189 the trigger is `["MadNLPGPU", "CUDA", "CUDSS"]` — all
three, or the extension stays inactive and `MadNLP{GPU}` is never registered,
silently. `MadNLPGPU.CUDSSSolver isa Type` is asserted outside any device gate.
The device tier has no honest local green, so it is `@test_skip` rather than an
elided `if is_cuda_on()` branch — a skip appears in the summary, a missing
branch does not.
One assertion here was self-sabotaging and is worth the comment it now carries:
`occursin("GPU", string(flow_type))` is vacuously true because the type string
embeds this module's own name, `TestGPURouting`. Matching `SciML{GPU` and
`DifferentiationInterface{GPU` instead also states the real claim — that one
token resolves *both* strategy families.
Docs cover the six user-visible breaks, leading with the one that changes every
example's preamble: `Flow` now requires an integrator `using`.
Refs #815
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adapts the runner contract from CTSolvers#190 (merged). Our device tier was `@test_skip` unconditionally, so a `kkt` runner whose driver broke was indistinguishable from a laptop: the GPU job would go green having run nothing. `on_gpu_runner()` reads `RUNNER_NAME`, set by the Actions agent itself, and turns the skip into an assertion on the one machine that must have a device. Inert off the runner — local run unchanged at 31 passed, 1 skipped. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 29, 2026
…and :sciml
`describe(:id)` previously only knew the solve registry (discretizer, NLP
modeler, NLP solver) — `:di` and `:sciml`, from CTFlows' flow registry, were
unreachable through the single-argument convenience wrapper. The integrator
and the AD backend are strategies in the control-toolbox sense like any
other, so their options should be inspectable the same way (.reports item).
`get_full_strategy_registry()` merges the two registries rather than falling
back with try/catch: a fallback would swallow the genuine "unknown strategy"
error on a typo and surface whatever the second registry raised instead. The
merge is total here because the two registries are measurably disjoint — no
shared id, no shared family, `:cpu`/`:gpu` bound to the same types on both
sides — asserted in test_describe.jl rather than assumed.
Reaching into StrategyRegistry's two fields to build the union is deliberate
but temporary, and skips the validation create_registry performs within one
registry — exactly what a cross-registry union should re-check. Requested
upstream as CTBase#517 (Base.merge for StrategyRegistry); switch once
released.
Along the way: describe(:sciml) turns out to throw FieldError on any
registry that contains it, and takes describe(:cpu)/describe(:gpu) down with
it — CTBase's _strategy_base_name only handles strategies with at most two
type parameters, and SciML has four. Root cause and fix are upstream
(CTBase#516); CTSolvers#191 tracks the missing describe coverage for the
Integrators family that let it through. The three affected assertions here
are @test_broken, not skipped, so the fix landing flips them to unexpected
passes and fails the suite — the reminder to promote them.
Full suite: 23 passed + 3 broken (new file), no regressions in the
registry-dependent groups (helpers/print, helpers/registry, helpers/methods,
reexport/{ctbase,ctsolvers,optimalcontrol}: 552/552).
Refs: control-toolbox/CTBase.jl#516, control-toolbox/CTBase.jl#517,
control-toolbox/CTSolvers.jl#191
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Matches the actual call sites — typeof(discretizer)/typeof(modeler)/ typeof(solver) are always AbstractDiscretizer/AbstractNLPModeler/ AbstractNLPSolver, all <: CTBase.Strategies.AbstractStrategy. Does not remove the need for the try/catch below it, and the docstring now says so explicitly: `parameter` is optional-to-override on any AbstractStrategy subtype, and Julia has no way to require an interface method at the abstract type's definition site. A third-party strategy that forgets the override still throws NotImplemented regardless of how tightly T is bounded here — see helpers/print.jl#331-337. test_print.jl: 87/87. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Not a silent absorption any more: a third-party strategy that never implements `CTBase.Strategies.parameter` still falls back to `nothing` (display must not crash the caller's solve over an optional, cosmetic detail), but now says so once per type via `@warn ... maxlog=1`, keyed on the strategy type so a solve loop doesn't spam. Confirmed via the existing FakeDiscretizerNoParam/FakeModelerNoParam/ FakeSolverNoParam fixtures in test_print.jl, which already exercise this exact path: one warning per type, test_print.jl still 87/87. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 29, 2026
Each indirect fixture (Goddard, the three double-integrator variants) now
carries its own shoot_builder — a closure of signature
`(; hamiltonian_type=:total) -> (shoot!, ξ_exact, ξ_guess)` — on TestProblem,
next to the problem itself. Quadrotor's stays `nothing`: no exploitable
extremal structure, and TestProblem's own constructor now enforces the claim
(`:indirect` requires shoot_builder !== nothing, not `data.p0`).
One generic suite/indirect/test_shooting_sweep.jl replaces three
hand-written files (test_goddard.jl, test_double_integrator_{time,energy}.jl),
looping over problems_for(:indirect) across both forms. It also adds the
Newton-from-a-perturbed-guess half none of the three ever exercised — only
the residual at the known reference was checked before.
Found along the way: the same derivations were ALSO written a second time in
suite/problems/test_hamiltonian_type.jl, independently. That file now
consumes the same shoot_builder(; hamiltonian_type=ht), keeping only what is
genuinely its own — sweeping :total/:partial and the "they agree/disagree"
assertions.
The one guard-rail assertion from the three deleted files that was not
already covered generically elsewhere (`variable=` mandatory when *omitted*
on a NonFixed flow) moved to test_flow_api.jl, next to its mirror image
("no variable on a Fixed flow") which already lived there. Everything else
they carried (constraint/multiplier pairing, trajectory form, Lift-vs-
Hamiltonian semantics) was redundant with existing generic coverage and is
gone rather than moved.
Full suite: 2166 passed, 5 broken (2 device/capability skips + 3 pre-existing
@test_broken in test_describe.jl, unrelated to this change), zero
regressions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eter Three requests filed against CTBase while working on describe()/parameter() support here (CTBase#516, #517, #518) all shipped in 0.28.8-beta. Consuming them removes the temporary workarounds each one was standing in for: - get_full_strategy_registry() now calls Base.merge(a::StrategyRegistry, bs::StrategyRegistry...) instead of reaching into .families/.parameters and merging the dicts by hand — the cross-registry validation create_registry already does within one registry now runs across the two being combined. - _strategy_parameter() forwards to the new non-throwing Strategies.parameter(T, default), passing a dedicated sentinel (_PARAMETER_NOT_IMPLEMENTED) as default so the @warn path (a strategy that never implemented the contract) stays distinguishable from a strategy that legitimately declares no parameter — the one case the 2-arg accessor itself can't tell apart, since both collapse to `nothing`. - describe(:sciml) no longer throws FieldError (CTBase#516 was _strategy_base_name breaking on strategies with 3+ type parameters). The three @test_broken in test_describe.jl — :sciml, :cpu, :gpu — are promoted to @test; WORKING/BROKEN collapses into a single ALL_STRATEGIES list. CTBase compat was already "0.28" (minor-pin per the house rule), so no Project.toml change; only the resolved version moved, 0.28.7-beta -> 0.28.8-beta. Full suite: 2168 passed, 2 broken (the two legitimate device/capability skips) — test_describe.jl 25/25, zero broken, down from 3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7 tasks
Consumes CTBase 0.28.9-beta: an open-loop control depends only on time, u(t) (or u(t, v)) — autonomy is a property of the OCP, not of the control, so OpenLoop no longer offers is_autonomous as a real choice the way ClosedLoop/DynClosedLoop do. is_autonomous is kept as a misuse-detector keyword that now warns rather than doing nothing. test_flow_api.jl: - "control-law kinds select different flows": drop the now-inert is_autonomous=false on the OpenLoop construction, rewrite the comment that described the old trap (an autonomous OpenLoop called with no arguments at all) since the trap no longer exists. - "is_autonomous governs the law's arity": the OpenLoop row of the arity table loses its autonomous variant — there is only one spelling now. Added a dedicated regression test that the old zero-argument spelling (OpenLoop(() -> 0.0)) still fails, loudly, with MethodError at integration time rather than constructing silently. BREAKING.md / CHANGELOG.md also completed with everything landed since the first v2.1.0-beta entries were written: describe() now covers the full strategy surface (:di, :sciml, merging the solve and flow registries via CTBase's Base.merge), the non-throwing parameter(T, default) accessor package authors can use instead of a hand-rolled try/catch, and this OpenLoop change. CTFlows' own six zero-argument OpenLoop test sites are fixed separately, on their own branch/PR, not here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ocots
marked this pull request as ready for review
July 30, 2026 08:39
Both jobs need use_ct_registry: true — this branch depends on beta versions (CTBase, CTFlows, CTSolvers, CTLie, ...) that only exist in the control-toolbox registry, not General. Drops the explicit julia `versions` pin and adds windows-latest to the CPU job's runner matrix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Julia 1.10's inference widens `_extract_kwarg`'s return type to
Union{Nothing,AbstractDiscretizer} instead of narrowing to the concrete
branch actually taken, making @inferred fail spuriously — a real
inference-precision difference between 1.10 and 1.11, not a bug in
_extract_kwarg. The allocation checks (@allocated) are unaffected and
still run unconditionally on both versions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Merged
2 tasks
Temporary — points the CPU job at fix/windows-pkg-cli-git instead of main so this branch's Windows CI can confirm CTActions#67 before it merges. Revert to @main once that PR lands. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Windows CI confirmed green on fix/windows-pkg-cli-git (git_cli input on julia-buildpkg). CTActions#67 is merged, so revert to @main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Same cause as the kwarg-extraction @inferred guard: Julia 1.10 has a measurably different allocation profile for the same code path on Windows (25760 vs the 21648 this bound was set for; 128800 vs 120000 for the 5x loop) — not a regression, just version drift in these bounds. Guard both assertions; the @allocated measurement itself still runs unconditionally on both versions. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
Author
|
Closes #815. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the load failure described in #815 and brings OptimalControl onto the restructured CTx stack.
Status: all phases (A–K) done and green. Full local suite: 2167 passed / 2 broken (expected) / 0 failed in 25m48s.
Context
OptimalControl was pinned at
CTBase = "=0.18.8"/CTModels = "=0.10.1"while the ecosystem was reorganised around the Handbookmodules.mdrule. The package did not load —UndefVarError: AbstractDiscretizer not defined in CTDirect.Three symbol families physically changed package:
CTFlowsCTBase.DataFlowdispatches onCTFlowsCTLiead(wasLie),Lift,Poisson,∂ₜ,@LieCTSolversCTBase.{Strategies,Options,Orchestration}CTDirectCTSolvers.DOCPAbstractDiscretizer,discretizeFull specification:
.reports/upgrade-v2.1.0-beta.mdCommits, one per phase
feat(imports)!: migrate to the restructured CTx stacktest(reexport): assert ownership, not mere definednesstest(helpers,builders): re-point paths, consolidate CUDA checkstest(solve): re-point paths, complete the strategy contract in mockstest(indirect)!: migrate to the new flow calling conventiontest(problems): dual-form library, declare direct/indirect capabilitytest: form equivalence, hamiltonian_type, flow API, shape contractfix: consume CTBase 0.28.9-beta — OpenLoop unconditionally non-autonomous (CTBase#515)ci: use ct-registry, add windows to the CPU matrixTwo traps worth flagging for review
Precompilation proves nothing here. Argument annotations are load-fatal, bodies fail only on first call — the package precompiles cleanly with half the migration missing. Every phase was verified by forcing the lazy bodies (registry,
describe, print helpers) and running a realsolveend to end.isdefinedis vacuously true forBasenames. The reexport group assertedisdefined(OptimalControl, :foo), which passes via the implicitusing Basefortime,merge,status,name,value,describe,success. The group could go green while OptimalControl re-exported none of them. Replaced with ownership checks (test/helpers/reexport.jl).Findings that were not in the plan
parameteris not a rename ofget_parameter_type. The old one returnednothingfor a strategy without a parameter; the new one throwsNotImplemented. This broke option routing for every mock strategy in the suite, and made_extract_strategy_parameterscrash on any third-party strategy that had not implemented the optional contract. Resolved via the new non-throwingCTBase.Strategies.parameter(strategy_type, default)accessor.OpenLoop/ClosedLoop/DynClosedLoopare factory functions, not types. They all build aControlLaw{F,Kind,…}; the kind is a trait parameter. Same for the three constraint kinds.OpenLoopis now unconditionally non-autonomous — an open-loop control has nothing but time, so autonomy was never a real choice for it the way it is forClosedLoop/DynClosedLoop. Raised neutrally as Should an autonomous OpenLoop control law take no argument at all? CTBase.jl#515, shipped in CTBase 0.28.9-beta (is_autonomouskept only as a warn-on-use misuse detector, no effect onOpenLoop). Companion fix for the 6 stale call sites in CTFlows: fix: OpenLoop is unconditionally non-autonomous (CTBase#515) CTFlows.jl#378.Flow(ocp, ClosedLoop/OpenLoop)returns a state flow, not aPreconditionErroras the report stated.successis dead upstream.CTModels.Solutionsexports the name but defines no method —success(sol)was always aMethodError. Dropped from the re-export;successfulis the real accessor.constraint/multiplierwithout its pair raisesIncorrectArgument, notPreconditionError.ocp_solutiontakes aBuiltModel, not aDiscretizedModel— the old assertion was wrong and passed by chance.describenow covers the full strategy surface (:di,:sciml, …) via the newBase.merge(::StrategyRegistry...)in CTBase.Test rework
Problems now build in two front-end forms (
@defand the functionalCTModels.BuildingAPI) and declare which solution methods they are fixtures for. The quadrotor has no exploitable extremal structure, so it is direct-only — and declaring:indirectwithout a referencep0is rejected by the constructor, so the claim cannot rot.Indirect test fixtures (Goddard, double integrator ×3) now carry their own
shoot_builderclosure, consumed generically by a shooting sweep and by thehamiltonian_typetests, rather than each problem re-deriving its own flows.New groups cover what OptimalControl actually owns rather than duplicating upstream:
hamiltonian_type—:totaland:partialagree wherever the law is stationary for H̃, which is everywhere in the indirect suite. The discriminating case (a deliberately non-stationary law) shows they are not redundant, and why.solvestack, the same contract on the indirect path (CTFlows only has defensiveisa Number ? … : …[1]guards, which tolerate either shape), and the two paths agreeing — the seam nothing upstream can check.@test_skipand gated behind the GPU-runner-required contract (kktself-hosted runner).Public API changes
See BREAKING.md. Lead item:
Flownow requires an integratorusing— it changes every example's preamble.CI
Both CPU and GPU jobs now set
use_ct_registry: true— this branch depends on beta versions (CTBase, CTFlows, CTSolvers, CTLie, ...) that only exist in the control-toolbox registry, not General.windows-latestadded to the CPU runner matrix.Out of scope, by design:
docs/(~24 stale pages, separate PR), the full 25-problem library behind a scheduled workflow, and GPU device-tier assertions (deferred to the GPU runner, marked@test_skipso they appear in the summary).🤖 Generated with Claude Code